import os
import argparse
import logging
import glob
import shutil
from datetime import timedelta
from comtypes.client import CreateObject, Constants
from pptx import Presentation
import azure.cognitiveservices.speech as speechsdk

# 可选依赖
try:
    import edge_tts
    import asyncio
    EDGE_TTS_AVAILABLE = True
except ImportError:
    EDGE_TTS_AVAILABLE = False
    print("Warning: edge-tts not available, will use other TTS engines")

import pyttsx3  # 本地语音引擎

# 配置日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)

# 临时文件目录
TEMP_DIR = "temp"

def setup_temp_dirs():
    """创建临时目录结构"""
    dirs = [os.path.join(TEMP_DIR, "slides"), os.path.join(TEMP_DIR, "audio")]
    for d in dirs:
        if not os.path.exists(d):
            os.makedirs(d)
            logger.info(f"创建临时目录: {d}")

def extract_ppt_text(ppt_path):
    """使用python-pptx提取PPT文本内容"""
    try:
        prs = Presentation(ppt_path)
        slides_text = []

        for slide in prs.slides:
            text = []
            for shape in slide.shapes:
                if hasattr(shape, "text") and shape.text.strip():
                    text.append(shape.text.strip())
            slides_text.append("\n".join(text))
        
        logger.info(f"成功提取 {len(slides_text)} 页PPT文本")
        return slides_text
    except Exception as e:
        logger.error(f"提取PPT文本失败: {str(e)}")
        raise

def export_slides_as_images(ppt_path):
    """使用COM接口导出PPT为图片"""
    import win32com.client
    import pythoncom
    import traceback
    
    output_dir = os.path.join(TEMP_DIR, "slides")
    powerpoint = None
    pres = None
    
    try:
        # 初始化COM对象
        powerpoint = win32com.client.Dispatch("PowerPoint.Application")
        powerpoint.Visible = 1  # 可视化模式
        
        # 打开PPT文件
        ppt_abs_path = os.path.abspath(ppt_path)
        logger.debug(f"打开PPT文件: {ppt_abs_path}")
        pres = powerpoint.Presentations.Open(ppt_abs_path)
        
        # 导出每张幻灯片
        image_paths = []
        for i in range(1, pres.Slides.Count + 1):
            output_path = os.path.abspath(os.path.join(output_dir, f"slide_{i-1}.png"))
            logger.debug(f"导出第 {i} 页幻灯片: {output_path}")
            pres.Slides(i).Export(output_path, "PNG")
            image_paths.append(output_path)
        
        logger.info(f"成功导出 {len(image_paths)} 页幻灯片图片")
        return sorted(image_paths, key=lambda x: int(os.path.basename(x).split('_')[-1].split('.')[0]))
        
    except Exception as e:
        # 添加详细的错误信息
        logger.error(f"PPT导出错误: {str(e)}")
        logger.error(traceback.format_exc())
        
        # 获取COM错误信息
        if hasattr(e, 'excepinfo') and len(e.excepinfo) > 5:
            hresult = pythoncom.GetScode(e.excepinfo[5])
            logger.error(f"COM错误代码: {hex(hresult)}")
            
            # 常见错误处理
            if hresult == 0x80080005:  # CO_E_SERVER_EXEC_FAILURE
                logger.error("请确保Microsoft PowerPoint已安装并激活")
            elif hresult == 0x80070005:  # E_ACCESSDENIED
                logger.error("访问被拒绝 - 请检查文件权限")
            else:
                logger.error("未知COM错误，请检查PowerPoint安装")
        else:
            logger.error("无法获取COM错误详细信息")
            
        raise
    finally:
        # 确保关闭所有资源
        try:
            if pres:
                pres.Close()
        except:
            pass
        try:
            if powerpoint:
                powerpoint.Quit()
        except:
            pass

def generate_audio(text_list, azure_key=None, azure_region="eastus", voice_name=None, local_voice_id=None,
                  use_edge_tts=True, edge_voice="zh-CN-XiaoxiaoNeural"):
    """生成音频文件，优先使用Edge-TTS，然后Azure语音服务，最后本地引擎"""
    try:
        output_dir = os.path.join(TEMP_DIR, "audio")
        
        audio_paths = []
        empty_count = 0
        
        # 优先尝试使用Edge-TTS（免费且高质量）
        if use_edge_tts and EDGE_TTS_AVAILABLE:
            try:
                for i, text in enumerate(text_list):
                    if not text.strip():
                        empty_count += 1
                        continue
                        
                    output_path = os.path.join(output_dir, f"audio_{i}.wav")
                    
                    # 使用Edge-TTS生成音频
                    async def generate_edge_audio():
                        communicate = edge_tts.Communicate(text, edge_voice)
                        await communicate.save(output_path)
                    
                    # 在事件循环中运行
                    try:
                        loop = asyncio.get_event_loop()
                    except RuntimeError:
                        loop = asyncio.new_event_loop()
                        asyncio.set_event_loop(loop)
                    
                    loop.run_until_complete(generate_edge_audio())
                    
                    audio_paths.append(f"audio_{i}.wav")
                    logger.debug(f"使用Edge-TTS生成第 {i+1} 页音频: {output_path}")
                
                logger.info(f"使用Edge-TTS成功生成 {len(audio_paths)} 个音频文件, 跳过 {empty_count} 个空文本")
                return audio_paths
            except Exception as edge_e:
                logger.warning(f"Edge-TTS不可用: {str(edge_e)}，回退到其他TTS引擎")
        
        # 尝试使用Azure语音服务
        if azure_key and azure_key != "YOUR_AZURE_KEY":
            try:
                speech_config = speechsdk.SpeechConfig(subscription=azure_key, region=azure_region)
                # 使用指定音色或默认音色
                speech_config.speech_synthesis_voice_name = voice_name if voice_name else "zh-CN-XiaoxiaoNeural"
                
                for i, text in enumerate(text_list):
                    if not text.strip():
                        empty_count += 1
                        continue
                        
                    output_path = os.path.join(output_dir, f"audio_{i}.wav")
                    audio_config = speechsdk.audio.AudioOutputConfig(filename=output_path)
                    synthesizer = speechsdk.SpeechSynthesizer(speech_config=speech_config, audio_config=audio_config)
                    
                    result = synthesizer.speak_text_async(text).get()
                    if result.reason == speechsdk.ResultReason.SynthesizingAudioCompleted:
                        # 仅返回文件名，不包含完整路径
                        audio_paths.append(f"audio_{i}.wav")
                        logger.debug(f"使用Azure生成第 {i+1} 页音频: {output_path}")
                    else:
                        logger.warning(f"Azure生成第 {i+1} 页音频失败: {result.reason}")
                        # Azure失败时回退到本地引擎
                        audio_paths.append(f"audio_{i}.wav")
                
                logger.info(f"使用Azure成功生成 {len(audio_paths)} 个音频文件, 跳过 {empty_count} 个空文本")
                return audio_paths
            except Exception as azure_e:
                logger.warning(f"Azure语音服务不可用: {str(azure_e)}")
        

        # 使用本地引擎
        for i, text in enumerate(text_list):
            if not text.strip():
                empty_count += 1
                continue
                
            output_path = os.path.join(output_dir, f"audio_{i}.wav")
            # 生成音频文件
            generate_local_audio(text, i, output_dir, voice_id=local_voice_id)
            audio_paths.append(f"audio_{i}.wav")
        
        logger.info(f"使用本地引擎成功生成 {len(audio_paths)} 个音频文件, 跳过 {empty_count} 个空文本")
        return audio_paths
        
    except Exception as e:
        logger.error(f"生成音频失败: {str(e)}")
        raise

def generate_local_audio(text, index, output_dir, voice_id=None):
    """使用本地pyttsx3引擎生成音频"""
    try:
        output_path = os.path.join(output_dir, f"audio_{index}.wav")
        engine = pyttsx3.init()
        
        # 设置音色
        if voice_id:
            engine.setProperty('voice', voice_id)
        
        engine.setProperty('rate', 150)  # 语速150词/分钟
        engine.setProperty('volume', 0.9)  # 音量
        engine.save_to_file(text, output_path)
        engine.runAndWait()
        logger.debug(f"使用本地引擎生成第 {index+1} 页音频: {output_path}")
        return output_path
    except Exception as e:
        logger.error(f"本地引擎生成第 {index+1} 页音频失败: {str(e)}")
        raise



def create_video(image_paths, audio_filenames, output_file="output.mp4"):
    """使用FFmpeg合成视频并清理临时文件"""
    try:
        # 确保所有路径使用正斜杠
        image_paths = [p.replace('\\', '/') for p in image_paths]
        
        # 构建正确的音频路径（使用temp/audio目录）
        audio_paths = [os.path.join("temp", "audio", name).replace('\\', '/') for name in audio_filenames]
        
        # 获取音频文件时长
        durations = []
        for audio_path in audio_paths:
            cmd = f'ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "{audio_path}"'
            duration = os.popen(cmd).read().strip()
            if duration:
                durations.append(float(duration))
            else:
                logger.error(f"无法获取音频文件时长: {audio_path}")
                raise ValueError(f"无法获取音频文件时长: {audio_path}")
        
        # 生成FFmpeg concat文件
        concat_file = "input.txt"
        with open(concat_file, 'w') as f:
            for i, img_path in enumerate(image_paths):
                f.write(f"file '{img_path}'\n")
                if i < len(image_paths) - 1:  # 最后一张幻灯片不需要duration
                    f.write(f"duration {durations[i]}\n")
        
        # 生成无声视频
        temp_video = "video_no_audio.mp4"
        os.system(f'ffmpeg -y -f concat -safe 0 -i "{concat_file}" -vsync vfr -pix_fmt yuv420p "{temp_video}"')
        
        # 生成音频文件列表
        audio_list = "audio_list.txt"
        with open(audio_list, 'w') as f:
            for audio_path in audio_paths:
                f.write(f"file '{audio_path}'\n")
        
        # 合并音频
        merged_audio = "output_audio.wav"
        os.system(f'ffmpeg -y -f concat -safe 0 -i "{audio_list}" -c copy "{merged_audio}"')
        
        # 合并视频和音频
        os.system(f'ffmpeg -y -i "{temp_video}" -i "{merged_audio}" -c:v copy -c:a aac -map 0:v:0 -map 1:a:0 "{output_file}"')
        
        logger.info(f"视频生成成功: {output_file}")
        return output_file
    except Exception as e:
        logger.error(f"合成视频失败: {str(e)}")
        raise

def cleanup_temp_files():
    """清理临时目录"""
    try:
        if os.path.exists(TEMP_DIR):
            shutil.rmtree(TEMP_DIR)
            logger.info("已清理临时文件")
    except Exception as e:
        logger.warning(f"清理临时文件失败: {str(e)}")

def main():
    parser = argparse.ArgumentParser(description="将PPT转换为带口播稿的视频")
    parser.add_argument("ppt_file", help="PPT文件路径")
    parser.add_argument("--output", default="output.mp4", help="输出视频文件名")
    parser.add_argument("--edge_voice", default="zh-CN-XiaoxiaoNeural", help="Edge-TTS音色名称（优先使用）")
    parser.add_argument("--disable_edge_tts", action="store_true", help="禁用Edge-TTS引擎")
    parser.add_argument("--azure_key", default=None, help="Azure语音服务密钥（可选）")
    parser.add_argument("--azure_region", default="eastus", help="Azure语音服务区域")
    parser.add_argument("--voice", default=None, help="Azure语音音色名称（如：zh-CN-YunyangNeural）")
    parser.add_argument("--local_voice", default=None, help="本地引擎音色ID（运行 python main.py --list_voices 查看可用音色）")
    parser.add_argument("--keep_temp", action="store_true", help="保留临时文件")
    args = parser.parse_args()

    try:
        # 初始化临时目录
        setup_temp_dirs()

        # 步骤1：提取文本
        logger.info("正在提取PPT文本...")
        slide_texts = extract_ppt_text(args.ppt_file)

        # 步骤2：导出图片
        logger.info("正在导出PPT图片...")
        slide_images = export_slides_as_images(args.ppt_file)

        # 步骤3：生成音频
        logger.info("正在生成语音文件...")
        audio_filenames = generate_audio(
            slide_texts, 
            azure_key=args.azure_key, 
            azure_region=args.azure_region,
            voice_name=args.voice,
            local_voice_id=args.local_voice,
            use_edge_tts=not args.disable_edge_tts,
            edge_voice=args.edge_voice
        )

        # 步骤4：合成视频
        logger.info("正在合成最终视频...")
        output = create_video(slide_images, audio_filenames, output_file=args.output)
        logger.info(f"处理完成! 输出视频: {output}")

    except Exception as e:
        logger.error(f"处理失败: {str(e)}")
    finally:
        # 清理临时文件
        if not args.keep_temp:
            cleanup_temp_files()

if __name__ == "__main__":
    # 添加列出可用音色的功能
    import sys
    
    if "--list_voices" in sys.argv:
        # Edge-TTS 常用音色列表
        print("\nEdge-TTS 常用中文音色：")
        edge_voices = [
            "zh-CN-XiaoxiaoNeural",  # 晓晓（女）
            "zh-CN-YunyangNeural",   # 云扬（男）
            "zh-CN-YunyeNeural",     # 云野（男）
            "zh-CN-XiaoyiNeural",    # 晓逸（女）
            "zh-CN-YunjianNeural",   # 云健（男）
            "zh-CN-XiaochenNeural",  # 晓辰（女）
            "zh-CN-XiaohanNeural",   # 晓涵（女）
            "zh-CN-XiaomengNeural",  # 晓梦（女）
            "zh-CN-XiaomoNeural",    # 晓墨（女）
            "zh-CN-XiaoqiuNeural",   # 晓秋（女）
            "zh-CN-XiaoruiNeural",   # 晓睿（女）
            "zh-CN-XiaoshuangNeural",# 晓双（女）
            "zh-CN-XiaoxuanNeural",  # 晓萱（女）
            "zh-CN-XiaoyanNeural",   # 晓颜（女）
            "zh-CN-XiaoyouNeural",   # 晓悠（女）
            "zh-CN-XiaozhenNeural",  # 晓甄（女）
            "zh-CN-YunxiNeural",     # 云希（男）
            "zh-CN-YunxiaNeural",    # 云夏（男）
            "zh-CN-YunjieNeural",    # 云杰（男）
            "zh-CN-YunyouNeural"     # 云悠（男）
        ]
        
        for voice in edge_voices:
            gender = "女" if "Xiao" in voice else "男"
            print(f"  {voice} ({gender})")
        
        # 本地引擎音色
        try:
            engine = pyttsx3.init()
            voices = engine.getProperty('voices')
            print("\n本地引擎音色列表：")
            for i, voice in enumerate(voices):
                print(f"{i}: ID={voice.id} | 名称={voice.name} | 语言={voice.languages[0] if voice.languages else '未知'}")
        except Exception as e:
            print(f"获取本地音色列表失败: {str(e)}")
        
        sys.exit(0)
    main()
